package com.gitee.qdbp.jdbc.sql.parse;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitee.qdbp.able.beans.KeyString;
import com.gitee.qdbp.able.exception.ServiceException;
import com.gitee.qdbp.jdbc.api.SqlBoot;
import com.gitee.qdbp.jdbc.exception.DbErrorCode;
import com.gitee.qdbp.jdbc.model.DbVersion;
import com.gitee.qdbp.jdbc.model.SqlFile;
import com.gitee.qdbp.jdbc.plugins.SqlFileScanner;
import com.gitee.qdbp.jdbc.sql.SqlBuffer;
import com.gitee.qdbp.staticize.common.IMetaData;
import com.gitee.qdbp.staticize.exception.TagException;
import com.gitee.qdbp.staticize.publish.TemplateTools;
import com.gitee.qdbp.staticize.tags.base.Taglib;
import com.gitee.qdbp.tools.compare.CompareTools;
import com.gitee.qdbp.tools.utils.ConvertTools;
import com.gitee.qdbp.tools.utils.StringTools;
import com.gitee.qdbp.tools.utils.VerifyTools;
import com.gitee.qdbp.tools.utils.VersionCodeTools;

/**
 * Sql片断的容器
 *
 * @author zhaohuihua
 * @version 20200830
 * @since 3.2.0
 */
public class SqlFragmentContainer {

    private static final Logger log = LoggerFactory.getLogger(SqlFragmentContainer.class);

    /** 插件容器 **/
    private final SqlBoot sqlBoot;
    /** 标签库 **/
    private final Taglib taglib;
    /** 扫描状态 (如果正在扫描, 则获取模板时需要加锁) **/
    private final ScanState scanState;
    /** 未指定数据库类型的SQL模板 **/
    // key=sqlId
    private final Map<String, SqlMetaDatas> untypedCache = new HashMap<>();
    /** 指定了数据库类型的SQL模板 **/
    // key=sqlId(dbType), value=[{version,SqlMetaDatas}], value按版本号从大到小排列, 无版本号的放最后
    private final Map<String, List<TypedSqlMetaData>> typedCache = new HashMap<>();

    public SqlFragmentContainer(SqlBoot sqlBoot) {
        this.sqlBoot = sqlBoot;
        this.scanState = new ScanState();
        this.taglib = sqlBoot.plugins().newSqlTaglib();
        this.taglib.setDefaultTagFactory(new SqlTagFactory(sqlBoot));
    }

    /**
     * 注册SQL模板标签元数据树
     *
     * @param scanTime 本次扫描时间
     * @param sqlId SQL编号
     * @param supports 支持哪些数据库版本
     * @param priority 优先级
     * @param sqlFile SQL文件信息
     * @param data 解析后的模板标签元数据树
     */
    protected void register(long scanTime, String sqlId, String supports, Integer priority, SqlFile sqlFile,
            IMetaData data) {
        SqlMetaData sqlMetaData = new SqlMetaData(scanTime, sqlId, supports, priority, sqlFile, data);
        if (supports == null || "*".equals(supports)) {
            // 存入未指定数据库类型的SQL模板容器
            if (untypedCache.containsKey(sqlId)) {
                untypedCache.get(sqlId).addItem(sqlMetaData);
            } else {
                untypedCache.put(sqlId, new SqlMetaDatas(scanState, sqlMetaData));
            }
        } else {
            // <supports>mysql.8,mariadb.10.2.2,postgresql,db2,sqlserver,sqlite.3.8.3</supports>
            String[] supportArray = StringTools.split(supports, ',');
            // 去重, key=supportItem
            Map<String, ?> supportMaps = new HashMap<>();
            for (String supportItem : supportArray) {
                if (supportItem.length() == 0) {
                    continue;
                }
                // 判断是否重复
                if (supportMaps.containsKey(supportItem)) {
                    continue;
                }
                supportMaps.put(supportItem, null);

                int dotIndex = supportItem.indexOf('.');
                String dbType;
                String version = null;
                if (dotIndex < 0) { // 不带版本号, 如mariadb
                    dbType = supportItem.toLowerCase();
                } else if (dotIndex == 0) { // .10.2.2?
                    continue;
                } else { // 带版本号, 如mariadb.10.2.2
                    dbType = supportItem.substring(0, dotIndex).trim().toLowerCase();
                    version = supportItem.substring(dotIndex + 1).trim();
                    if (version.length() == 0) {
                        version = null;
                    }
                }
                String cacheKey = sqlId + '(' + dbType + ')';
                TypedSqlMetaData value = new TypedSqlMetaData(scanState, version, sqlMetaData);
                if (this.typedCache.containsKey(cacheKey)) {
                    List<TypedSqlMetaData> sqlMetaDatas = this.typedCache.get(cacheKey);
                    mergeTagDatas(version, sqlMetaData, sqlMetaDatas);
                } else {
                    List<TypedSqlMetaData> sqlMetaDatas = ConvertTools.toList(value);
                    this.typedCache.put(cacheKey, sqlMetaDatas);
                }
            }
        }
    }

    // 合并SQL片段, 返回被替换的对象
    private void mergeTagDatas(String version, SqlMetaData newer, List<TypedSqlMetaData> list) {
        // value按版本号从大到小排列, 无版本号的放最后
        for (int index = 0; index < list.size(); index++) {
            TypedSqlMetaData target = list.get(index);
            String targetVersion = target.getMinVersion();
            if (targetVersion == null || "*".equals(targetVersion)) {
                if (version == null || "*".equals(version)) {
                    // 当前版本是*, 目标版本也是*, 合并到一起; 获取时会报冲突
                    target.getMetaDatas().addItem(newer);
                    return;
                } else {
                    // 当前版本不是*, 目标版本是*, 插入目标版本前面
                    list.add(index, new TypedSqlMetaData(scanState, version, newer));
                    return;
                }
            } else if (version == null || "*".equals(version)) {
                // 当前版本是*, 目标版本不是*, 不匹配
                continue;
            } else if (VersionCodeTools.compare(targetVersion, version) == 0) {
                // 遇到完全相同的版本, 合并到一起; 获取时会报冲突
                target.getMetaDatas().addItem(newer);
                return;
            } else if (VersionCodeTools.compare(targetVersion, version) < 0) {
                // 插入首个小于当前版本的位置的前面
                list.add(index, new TypedSqlMetaData(scanState, version, newer));
                return;
            }
        }
        // 没匹配上, 添加到最后
        list.add(new TypedSqlMetaData(scanState, version, newer));
    }

    /**
     * 查找SQL模板标签元数据树
     *
     * @param sqlId SQL编号
     * @return 标签元数据树
     */
    public IMetaData find(String sqlId) {
        return find(sqlId, true);
    }

    protected IMetaData find(String sqlId, boolean throwOnNotFound) {
        this.initSqlFiles();
        DbVersion dbVersion = sqlBoot.sqlDialect().dbVersion();
        String dbType = dbVersion.getDbType().name().toLowerCase();
        String currVersion = dbVersion.getVersionCode();

        SqlMetaDatas found = null;
        // 记录下尝试过哪些模板, 用于匹配失败时输出日志
        // key=dbType+dbVersion, value=location
        List<KeyString> tryedLocations = new ArrayList<>();

        // 开始查找模板
        String cacheKey = sqlId + '(' + dbType + ')';
        List<TypedSqlMetaData> typedList = typedCache.get(cacheKey);
        if (typedList != null && !typedList.isEmpty()) {
            // 逐一对比此模板的最低版本要求
            for (TypedSqlMetaData item : typedList) {
                String minVersion = item.getMinVersion();
                SqlMetaDatas datas = item.getMetaDatas();
                if (minVersion == null || "*".equals(minVersion)) {
                    found = datas; // 没有最低要求, 则当前数据库为匹配
                    break;
                }
                if (VersionCodeTools.compare(currVersion, minVersion) >= 0) {
                    found = datas; // 满足最低要求, 匹配成功
                    break;
                }
                for (SqlMetaData temp : datas.getItems()) {
                    tryedLocations.add(new KeyString(dbType + '.' + minVersion, temp.getMetaData().getRealPath()));
                }
            }
        }
        // 带版本的模板未匹配成功, 判断是否存在不带版本要求的模板
        if (found == null && untypedCache.containsKey(sqlId)) {
            found = untypedCache.get(sqlId);
        }

        String versionString = dbVersion.toVersionString();
        if (found != null) {
            if (!found.isConflicting()) {
                // 未冲突, 查找成功
                return found.getMajorItem().getMetaData();
            }
            // 有冲突, 输出日志
            StringBuilder conflicts = new StringBuilder();
            for (SqlMetaData item : found.getConflicts()) {
                conflicts.append("\n\t").append(item.getMetaData().getRealPath());
                String supports = item.getSupports();
                if (supports != null && !"*".equals(supports)) {
                    conflicts.append(' ').append("<supports>").append(item.getSupports()).append("</supports>");
                }
                if (item.getPriority() != null) {
                    conflicts.append(' ').append('(').append(item.getPriority()).append(')');
                }
            }
            int conflictSize = found.getConflicts().size();
            StringBuilder details = new StringBuilder();
            details.append("\nsqlId=").append(sqlId).append(", dbVersion=").append(versionString);
            details.append("\nexpected single template but found ").append(conflictSize).append(":");
            details.append(conflicts);
            if (log.isWarnEnabled()) {
                log.warn("Sql template '{}({})' conflict: {}", sqlId, versionString, details);
            }
            if (throwOnNotFound) {
                throw new ServiceException(DbErrorCode.DB_SQL_FRAGMENT_CONFLICT, details.toString());
            } else {
                return null;
            }
        }

        // 未匹配成功
        if (!throwOnNotFound) {
            return null;
        }

        // 抛出异常
        StringBuilder details = new StringBuilder();
        if (tryedLocations.isEmpty()) {
            details.append("sqlId=").append(sqlId).append(", dbVersion=").append(versionString);
            if (log.isWarnEnabled()) {
                log.warn("Sql template not found: {}", details);
            }
        } else {
            details.append("\nsqlId=").append(sqlId).append(", dbVersion=").append(versionString);
            details.append("\nmatches tryed locations:\n").append(locationsToString(tryedLocations));
            if (log.isWarnEnabled()) {
                log.warn("Sql template '{}({})' not found: {}", sqlId, versionString, details);
            }
        }
        throw new ServiceException(DbErrorCode.DB_SQL_FRAGMENT_NOT_FOUND, details.toString());
    }

    private String locationsToString(List<KeyString> locations) {
        int maxVersionLength = 0;
        for (KeyString item : locations) {
            if (maxVersionLength < item.getKey().length()) {
                maxVersionLength = item.getKey().length();
            }
        }
        StringBuilder buffer = new StringBuilder();
        for (KeyString item : locations) {
            if (buffer.length() > 0) {
                buffer.append('\n');
            }
            buffer.append('\t');
            buffer.append(StringTools.pad(item.getKey(), ' ', false, maxVersionLength));
            buffer.append(" --> ");
            buffer.append(item.getValue());
        }
        return buffer.toString();
    }

    /**
     * 判断SQL模板是否存在
     *
     * @param sqlId SQL编号
     * @return 是否存在
     */
    public boolean exist(String sqlId) {
        this.initSqlFiles();
        if (untypedCache.containsKey(sqlId)) {
            return true;
        }
        IMetaData metadata = find(sqlId, false);
        return metadata != null;
    }

    /** 根据SqlId从缓存中获取SQL模板, 渲染为SqlBuffer对象 **/
    public SqlBuffer generate(String sqlId, Map<String, Object> data) {
        IMetaData tags = find(sqlId);
        return render(tags, data);
    }

    /** 解析SQL模板内容, 渲染为SqlBuffer对象 **/
    public SqlBuffer render(String sqlString, Map<String, Object> data) {
        IMetaData tags;
        try {
            tags = TemplateTools.parse(sqlString, taglib);
        } catch (Exception e) {
            throw new ServiceException(DbErrorCode.DB_SQL_FRAGMENT_PARSE_ERROR, e);
        }
        return render(tags, data);
    }

    /** 将SQL模板渲染为SqlBuffer对象 **/
    public SqlBuffer render(IMetaData tags, Map<String, Object> data) {
        SqlBufferPublisher publisher = new SqlBufferPublisher(tags, sqlBoot);
        try {
            SqlBuffer buffer = publisher.publish(data);
            return buffer.indentAll(0).trim();
        } catch (TagException e) {
            throw new ServiceException(DbErrorCode.DB_SQL_FRAGMENT_RENDER_ERROR, e);
        } catch (IOException e) {
            throw new ServiceException(DbErrorCode.DB_SQL_FRAGMENT_RENDER_ERROR, e);
        }
    }

    private final Lock initlock = new ReentrantLock();
    private boolean inited = false;
    private final Lock scanlock = new ReentrantLock();
    private Long lastScanTime;

    /** 初始化SQL模板文件 **/
    public void initSqlFiles() {
        if (this.inited) {
            return;
        }
        this.initlock.lock();
        try {
            if (!this.inited) {
                this.scanState.setScanning(false);
                this.scanSqlFiles();
                this.inited = true;
            }
        } finally {
            this.initlock.unlock();
        }
    }

    /** 扫描SQL模板文件 **/
    public void scanSqlFiles() {
        if (this.scanState.isScanning()) {
            return;
        }

        if (this.scanlock.tryLock()) {
            try {
                if (this.scanState.isScanning()) {
                    return;
                }
                this.scanState.setScanning(true);
                try {
                    long now = System.currentTimeMillis();
                    this.doScanSqlFiles(now);
                    this.lastScanTime = now;
                } finally {
                    this.scanState.setScanning(false);
                }
            } finally {
                this.scanlock.unlock();
            }
        }
    }

    /** 扫描SQL模板文件 **/
    private void doScanSqlFiles(long scanTime) {
        SqlFileScanner scanner = sqlBoot.plugins().getSqlFileScanner();
        List<SqlFile> sqlFiles;
        try {
            Date lastScanTime = this.lastScanTime == null ? null : new Date(this.lastScanTime);
            sqlFiles = scanner.scanSqlFiles(lastScanTime);
        } catch (IOException e) {
            log.warn("Failed to scan sql template file", e);
            return;
        }

        Date startTime = new Date();

        // 解析SQL文件
        SqlFragmentParser parser = new SqlFragmentParser(taglib, sqlBoot.plugins());
        List<ParsedFragment> container = parser.parseSqlFiles(sqlFiles);

        for (ParsedFragment i : container) {
            register(scanTime, i.getSqlId(), i.getSupports(), i.getPriority(), i.getSqlFile(), i.getMetaData());
            if (i.getAlias() != null) {
                register(scanTime, i.getAlias(), i.getSupports(), i.getPriority(), i.getSqlFile(), i.getMetaData());
            }
        }

        if (log.isTraceEnabled() && !container.isEmpty()) {
            List<String> fragments = new ArrayList<>();
            for (ParsedFragment item : container) {
                fragments.add(item.toString());
            }
            String details = ConvertTools.joinToString(fragments, "\n\t");
            String msg = "Success to parse sql templates, elapsed time {}, total of {} fragments in {} files.\n\t{}";
            log.trace(msg, ConvertTools.toDuration(startTime, true), container.size(), sqlFiles.size(), details);
        } else if (log.isInfoEnabled()) {
            String msg = "Success to parse sql templates, elapsed time {}, total of {} fragments in {} files.";
            log.info(msg, ConvertTools.toDuration(startTime, true), container.size(), sqlFiles.size());
        }
    }

    private static class ScanState {
        private boolean scanning;

        public boolean isScanning() {
            return scanning;
        }

        public void setScanning(boolean scanning) {
            this.scanning = scanning;
        }
    }

    protected static class SqlMetaData implements Comparable<SqlMetaData> {

        /** SqlId **/
        private final String sqlId;
        /** 支持的版本 **/
        private final String supports;
        /** 优先级 **/
        private final Integer priority;
        /** 绝对路径 **/
        private final String absolutePath;
        /** 相对路径 **/
        private final String relativePath;
        /** 扫描时间 **/
        private final long scanTime;
        /** 最后更新时间 **/
        private final Long lastUpdateTime;
        /** 模板数据 **/
        private final IMetaData metadata;

        public SqlMetaData(long scanTime, String sqlId, String supports, Integer priority, SqlFile sqlFile,
                IMetaData metadata) {
            this.scanTime = scanTime;
            this.sqlId = sqlId;
            this.supports = supports;
            this.priority = priority;
            this.absolutePath = sqlFile.getAbsolutePath();
            this.relativePath = sqlFile.getRelativePath();
            this.lastUpdateTime = sqlFile.getLastUpdateTime();
            this.metadata = metadata;
        }

        /** SqlId **/
        public String getSqlId() {
            return sqlId;
        }

        /** 支持的版本 **/
        public String getSupports() {
            return supports;
        }

        public Integer getPriority() {
            return priority;
        }

        /** 扫描时间 **/
        public long getScanTime() {
            return scanTime;
        }

        /** 绝对路径 **/
        public String getAbsolutePath() {
            return absolutePath;
        }

        /** 相对路径 **/
        public String getRelativePath() {
            return relativePath;
        }

        /** 最后更新时间 **/
        public Long getLastUpdateTime() {
            return lastUpdateTime;
        }

        /** 模板数据 **/
        public IMetaData getMetaData() {
            return metadata;
        }

        public boolean isSamePriorityOf(SqlMetaData that) {
            if (that == null) {
                return false;
            }
            if (VerifyTools.notEquals(this.getPriority(), that.getPriority())) {
                return false;
            }
            boolean thisInJar = this.isPathInJar();
            boolean thatInJar = that.isPathInJar();
            return thisInJar == thatInJar;
        }

        @Override
        public int compareTo(SqlMetaData that) {
            if (this == that) {
                return 0;
            }
            if (that == null) {
                return -1;
            }

            // 按优先级降序
            int priority = CompareTools.descCompare(this.getPriority(), that.getPriority());
            if (priority != 0) {
                return priority;
            }

            boolean thisInJar = this.isPathInJar();
            boolean thatInJar = that.isPathInJar();
            if (thisInJar != thatInJar) {
                return thisInJar ? 1 : -1;
            }

            return 0;
        }

        protected boolean isPathInJar() {
            // jar:file:/D:/qdbp-jdbc/jdbc-test/src/test/resources/libs/xxx.jar!/settings/sqls/select.001.sql
            return getAbsolutePath().contains("jar:");
        }

        @Override
        public String toString() {
            StringBuilder buffer = new StringBuilder();
            buffer.append(sqlId);
            buffer.append('(').append(supports == null ? "*" : supports).append(')');
            if (priority != null) {
                buffer.append('(').append(priority).append(')');
            }
            buffer.append(' ').append(metadata.getRealPath());
            return buffer.toString();
        }
    }

    // 只有同一SqlId相同DbType相同版本才会进入此集合
    // 如果正在扫描, 则获取模板时需要加锁, 以避免外界取到doAddItem执行到半途的数据
    protected static class SqlMetaDatas {
        private final ScanState scanState;
        private final Lock scanLock = new ReentrantLock();
        private final List<SqlMetaData> metadatas = new ArrayList<>();
        private SqlMetaData majorItem;
        private List<SqlMetaData> conflictItems;

        public SqlMetaDatas(ScanState scanState, SqlMetaData metadata) {
            this.scanState = scanState;
            this.metadatas.add(metadata);
            this.majorItem = metadata;
        }

        public List<SqlMetaData> getItems() {
            boolean scanning = scanState.isScanning();
            if (scanning) {
                this.scanLock.lock();
            }
            try {
                return metadatas;
            } finally {
                if (scanning) {
                    this.scanLock.unlock();
                }
            }
        }

        public SqlMetaData getMajorItem() {
            boolean scanning = scanState.isScanning();
            if (scanning) {
                this.scanLock.lock();
            }
            try {
                return majorItem;
            } finally {
                if (scanning) {
                    this.scanLock.unlock();
                }
            }
        }

        public boolean isConflicting() {
            boolean scanning = scanState.isScanning();
            if (scanning) {
                this.scanLock.lock();
            }
            try {
                return conflictItems != null;
            } finally {
                if (scanning) {
                    this.scanLock.unlock();
                }
            }
        }

        public List<SqlMetaData> getConflicts() {
            boolean scanning = scanState.isScanning();
            if (scanning) {
                this.scanLock.lock();
            }
            try {
                return conflictItems;
            } finally {
                if (scanning) {
                    this.scanLock.unlock();
                }
            }
        }

        // 合并SQL片段到集合中
        public void addItem(SqlMetaData metadata) {
            boolean scanning = scanState.isScanning();
            if (scanning) {
                this.scanLock.lock();
            }
            try {
                this.doAddItem(metadata);
            } finally {
                if (scanning) {
                    this.scanLock.unlock();
                }
            }
        }

        private void doAddItem(SqlMetaData newer) {
            // 删除之前扫描的同一文件中相同ID的SQL片段
            Iterator<SqlMetaData> iterator = metadatas.iterator();
            while (iterator.hasNext()) {
                SqlMetaData older = iterator.next();
                if (older.getAbsolutePath().equals(newer.getAbsolutePath())) {
                    if (older.getScanTime() < newer.getScanTime()) {
                        iterator.remove();
                    }
                }
            }
            metadatas.add(newer);

            // 检查冲突
            if (metadatas.size() == 1) {
                conflictItems = null;
                majorItem = metadatas.get(0);
            } else if (metadatas.size() > 1) {
                // 按优先级排序
                Collections.sort(metadatas);
                // 取出优先级最高的第1个
                SqlMetaData first = metadatas.get(0);
                // 检查有没有优先级与之相同的
                List<SqlMetaData> same = new ArrayList<>();
                for (int i = 1; i < metadatas.size(); i++) {
                    SqlMetaData item = metadatas.get(i);
                    if (first.isSamePriorityOf(item)) {
                        same.add(item);
                    }
                }
                if (same.isEmpty()) {
                    conflictItems = null;
                    majorItem = first;
                } else { // 存在冲突
                    majorItem = null;
                    same.add(0, first);
                    conflictItems = same;
                }
            }
        }
    }

    protected static class TypedSqlMetaData {

        /** 最低版本要求 **/
        private final String minVersion;
        private final SqlMetaDatas metadatas;

        public TypedSqlMetaData(ScanState scanState, String minVersion, SqlMetaData metadata) {
            this.minVersion = minVersion;
            this.metadatas = new SqlMetaDatas(scanState, metadata);
        }

        /** 最低版本要求 **/
        public String getMinVersion() {
            return minVersion;
        }

        public SqlMetaDatas getMetaDatas() {
            return metadatas;
        }
    }

    protected static class ParsedFragment {

        private final String sqlId;
        private final String alias;
        private final String supports;
        private final Integer priority;
        private final SqlFile sqlFile;
        private final IMetaData metadata;

        public ParsedFragment(String sqlId, String alias, String supports, Integer priority, SqlFile sqlFile,
                IMetaData data) {
            this.sqlId = sqlId;
            this.alias = alias;
            this.supports = supports;
            this.priority = priority;
            this.sqlFile = sqlFile;
            this.metadata = data;
        }

        public String getSqlId() {
            return sqlId;
        }

        public String getAlias() {
            return alias;
        }

        public String getSupports() {
            return supports;
        }

        public Integer getPriority() {
            return priority;
        }

        public SqlFile getSqlFile() {
            return sqlFile;
        }

        public IMetaData getMetaData() {
            return metadata;
        }

        @Override
        public String toString() {
            StringBuilder buffer = new StringBuilder();
            buffer.append(sqlId);
            buffer.append('(').append(supports == null ? "*" : supports).append(')');
            if (priority != null) {
                buffer.append('(').append(priority).append(')');
            }
            buffer.append(' ').append(metadata.getRealPath());
            return buffer.toString();
        }
    }
}
